今天我們要來講解 Flutter 的基礎Widget。
Flutter 預設提供了兩種熱門的 UI 組件,分別是接近 Andorid 原生風格的 Material 以及接近 iOS 原生風格的 Cupertino 可供開發者使用,使得在開發者可以透過呼叫這些定義好的 UI 組件,快速的組合出應用程式。
所以讓我們再重新的檢視一下昨天我們最後寫的 Hello World 程式碼
// 引入 flutter 預載好的 material.dart UI 庫
import 'package:flutter/material.dart';
// 應用程式起始點
void main() {
// 開始執行應用程式,並呼叫 MaterialApp 建構子,表示我的應用程式為 Material style
runApp(MaterialApp(
title: 'Flutter Tutorial',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Playground'),
),
body: Center(
child: Text('Hello World'),
),
),
));
}
我們使用了 Material 作為我們預設的樣式,接著就是不斷的找尋是否有適合的 widget 可以讓我們進行呼叫,開始組合與微調。
由於篇幅因素,內容並無法涵蓋所有的 widget,以下我們將介紹幾個基礎的 widget 使用方法,除了持續壯大我們的 Flutter playground,更藉此能一步步的熟悉 flutter widget 使用方式,最終你就可以自己試著對照文件將各式各樣的 widget 加入到你的應用程式中。
顧名思義 Text 是用於顯示文字的工具,也可以針對文字進行樣式調整、對齊位置等等。我們需要使用到一個 widget 類別時,首先一定得先使用建構子來初始化我們將要宣告的物件。讓我們來看看 Text 的建構子是如何進行定義的
VS Code 檢視 widget class 的使用方式有兩種:
- 將滑鼠的游標移至該 widget 上方,VS Code 便會跳出一個小視窗,裡面會寫定義
- 在 widget 上方點選右鍵 並選擇「移至定義」就會導向定義該 widget 的檔案中
在我的 Flutter 版本中對於Text建構子如下,不同版本的可能會有些許出入不過並不影響我們怎麼閱讀此元件。在Text類別的定義中,第一個參數無需具名但是一定要夾帶;剩下用{}括起來的便是前面章節中學到的named parameters皆可選擇性的夾帶。
const Text(
String this.data, {
super.key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
this.textWidthBasis,
this.textHeightBehavior,
this.selectionColor,
}) : ... 後面省略
因此當我們在宣告最基本款的 Text widget 時,我們的宣告方式如下
Text('Hello World');
如此便成功的宣告了一個帶有 Hello World 訊息的 Text widget 。那如果要針對字體進行微調呢?我們來看看上面的 named parameters 中哪個最像是用來定義字體樣式的,應該很好猜就是 style 這個欄位。如果你是從寫前端過來的,當我想將文字樣式改為藍色 且大小為 18px 時,你可能會覺得 style 會是這樣設定:
Text('Hello World', style: 'font-size: 18px; color: blue');
注意!我們在看類別定義時千萬可別漏看了對應的型態!讓我們再一次的前往檢視 style 的型態 (方式與剛剛介紹的相同)。
final TextStyle? style;
首先因為 style 是屬於 optional parameters,因此 TextStyle? 表示賦予的值可為 TextStyle 型態或 null ,表示我們今天要用到 style 時就要找一個型態為 TextStyle 的東西來塞。這時我們再繼續往 TextStyle 的定義往下找:
const TextStyle({
this.inherit = true,
this.color,
this.backgroundColor,
this.fontSize,
this.fontWeight,
this.fontStyle,
this.letterSpacing,
this.wordSpacing,
this.textBaseline,
this.height,
this.leadingDistribution,
this.locale,
this.foreground,
this.background,
this.shadows,
this.fontFeatures,
this.fontVariations,
this.decoration,
this.decorationColor,
this.decorationStyle,
this.decorationThickness,
this.debugLabel,
String? fontFamily,
List<String>? fontFamilyFallback,
String? package,
this.overflow,
}) : ...後面省略
我們來鎖定我們所需要的屬性(字體大小改為 18px;顏色改為藍色)。上方最符合的就是 fontSize 與 color ,兩者分別的型態為 double 與 Color 。因此我們就填入相應所需要型態的值。最終結果如下:
Text('Hello World', style: TextStyle(fontSize: 18, color: Colors.blue));
花了一點篇幅帶大家從 Text 的定義開始像洋蔥一樣一層一層的往裡面剝開,找到我們所需要的屬性再依據所需要的型態給相應的值。其實其他的 widget 也都大同小異,只要掌握到此方法,便可以融會貫通到其他的地方拉!
是由 Material 所定義的按鈕,因此欲引用此 widget 記得要先引入 material package 。我們現在希望可以建立一個 Button ,其中的文字顯示方才的 Hello World 字樣。因此請先將剛剛的文字註解掉,並輸入 ElevatedButton 此時你的 VS Code 應該會跳出自動補全的提示,請按下 Enter ,這時該行應該會變成。
ElevatedButton(onPressed: onPressed, child: child)
我們一樣按照慣例別急著寫,先來檢視 ElevatedButton 的建構子,看看我們需要提供哪些資訊。
const ElevatedButton({
super.key,
required super.onPressed,
super.onLongPress,
super.onHover,
super.onFocusChange,
super.style,
super.focusNode,
super.autofocus = false,
super.clipBehavior = Clip.none,
super.statesController,
required super.child,
});
在類別建構子中全部都是 named parameters ,但因為 onPressed 與 child 兩個參數有標上 required ,也就表示建構此物件時一定要夾帶此兩個參數。
我們就字面上解析這兩個參數的意義:
child:型態為 Widget? 表示這個 button 底下要放 widget,會被包在此按鈕當中onPressed :型態為 VoidCallback? ,再往下翻你會發現 typedef VoidCallback = void Function(),也就是代表 VoidCallback 本身其實是 Function 的另一種寫法。因此 onPressed 是一個函式,用於表示點擊該按鈕後要觸發的事件函式。
typedef:是用來自定義型別的關鍵字,如:typedef IntList = List<int>便是定義IntList這個關鍵字可以用於表示List<int>這個型態
因此我們就將剛剛的 Text 放入 ElevatedButton 的 child 欄位中。並且希望每次按下按鈕時,都在終端機印出 Hello World 的字樣。
import 'package:flutter/material.dart';
// 應用程式起始點
void main() {
runApp(MaterialApp(
title: 'Flutter Tutorial',
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter Playground'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
debugPrint('Hello World');
},
child: const Text('Hello World',
style: TextStyle(fontSize: 18, color: Colors.white)))),
),
));
}
最後你的結果應該要長這樣,功能可以正常執行,並且編輯器沒有跳出任何的錯誤或是警告。
相信你剛剛在修改的過程當中一定遇到很多次終端機跟你提示「請使用 const 建構子來增進效能」的字樣,原因是因為之前我們有介紹到 const 創建的內容是不變的,表示使用 const 修飾的 widget 在應用程式的生命週期內只會建構一次,使得性能可以有相當大程度的優化。
因此當你的某個部件確定是不會變動的,請使用 const 來進行修飾。不過當你無法判斷時,相信你的編輯器提示會非常「好心」的跳出提示來告訴你,直到你改對為止XD
目前為止,我們介紹了兩種 widget 都僅限於呼叫 widget 並顯示於頁面上。但如果我想要將這些內容進行佈局呢? 這時候就需要一系列的佈局工具 (Layout widget)拉~
其實一開始的程式碼,在 ElevatedButton 之外有使用到 Center 我們還沒有提到。
在 Flutter 中有一個詞為 constraint (約束),指的是在渲染佈局中的限制或是規則,用於確定 widget 的大小和位置。這樣的約束關係是經由 parent 與 child 經由溝通確認後,再由 parent 傳遞給 child 的,告訴子組件應該要如何進行佈局。一旦子組件超越了 constraint 就會跳出錯誤,表示父組件無法容納。
Center 這個 widget 會根據 constraint 將 child 放置於正中間的位置,如我們的範例一樣。
Padding 是可以用於為子組件添加空白區域,用於調整與周圍的間距。嘗試將方才的 Center widget 拿掉,我們換上 Padding 來看看
Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton(
onPressed: () {
debugPrint('Hello World');
},
child: const Text('Hello World',
style: TextStyle(fontSize: 18, color: Colors.white))))
我們將 Center 拿掉後,ElevatedButton 會回到預設到左上方 (佈局的預設是由左至右、由上而下),我們設定 child 的 button 四周皆間隔 16 px 的距離。
將多個子組件(children)以水平方向進行排列,不過要注意的是 Row本身並不具備 scroll 的性質,因此在使用前需要先思考是否 children 的寬度足以被 Row 所容納,否則會產生錯誤。請替換成以下程式碼,看看會發生什麼事情
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(children: [
Container(color: Colors.red, width: 100, height: 50),
Container(color: Colors.indigo, width: 100, height: 60),
Container(color: Colors.green, width: 100, height: 70),
]))
原則上你會看到如下圖的運行結果。
我們宣告了三個等寬但高度不同的三個 Container 以水平方向進行排列。現在 Row :
- 寬度:螢幕寬度減去左右的16px padding
- 高度:找出最大高度的 child
這時我們來看看 Row 底下的除了 children 外,兩個也很重要用於對齊的參數:
mainAxisAlignment :表示水平方向的對齊屬性,預設為 start 表示靠左對齊。其他還有如 end 靠右對齊、center 水平置中對齊等等的屬性crossAxisAlignment :表示垂直方向的對齊屬性,預設為 center 垂直置中。其他還有如 start 垂直靠上、end 垂直靠下等等的屬性各位可以試著玩玩看,相信你很快就能了解所有屬性!這裡出個小小練習,會在文末進行解答喔~
請修改上述程式碼,使得顯示效果可以如下圖。
與 Row 相對的,Colummn 是將多個子組件(children)以垂直方向進行排列,同樣的是 Column本身並不具備 scroll 的性質,因此在使用前需要先思考是否 children 的長度足以被 Column 所容納,否則會產生錯誤。請替換成以下程式碼,看看會發生什麼事情
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
Container(color: Colors.red, width: 50, height: 100),
Container(color: Colors.indigo, width: 60, height: 100),
Container(color: Colors.green, width: 70, height: 100),
]))
原則上你會看到如下圖的運行結果。
我們宣告了三個等高但寬度不同的三個 Container 以垂直方向進行排列。現在 Column :
- 高度:螢幕高度減去上下的16px padding
- 寬度:找出最大寬度的 child
同樣我們也來看看 Column 底下的除了 children 外,兩個也用於對齊的參數:
- mainAxisAlignment :表示垂直方向的對齊屬性,預設為 start 表示靠上對齊。其他還有如 end 靠下對齊、center 垂直置中對齊等等的屬性
- crossAxisAlignment :表示水平方向的對齊屬性,預設為 center 水平置中。其他還有如 start 水平靠左、end 水平靠右等等的屬性
在 Row 的時候 mainAxisAlignment 中文直翻就是在主要軸的對齊屬性,也就是水平軸;crossAxisAlignment 就是轉 90 度交叉的垂直軸的對齊屬性。
換到 Column 的時候就完全相反,mainAxisAlignment 是垂直軸的對齊屬性;crossAxisAlignment 就是相對的水平軸對齊屬性。
這裡我們也一樣出個練習題,作為本篇的結尾。
請撰寫程式碼,使得顯示效果可以如下圖。這題會同時混用到 Row 與 Column,可以從 Column 方向開始思考。
今天我們認識:
Text 用於基礎顯示文字ElevatedButton 用於顯示按鈕,並認識運用 onPressed 來觸發事件Center 將子組件完全的置中Padding 設定子組件的間隔Row 將多個子組件以水平方向排列;Column 則是將多個子組件以垂直方向排列俗話說「給你魚吃,不如教你釣魚」。我們並無法全盤的介紹一輪所有 widget 的使用方式,但藉由今天的介紹相信大家已經具備了查找文件的能力拉(釣魚🐟)!只要找到好的 widget,看一下文件就能夠引用在你的應用程式中了。
另外Flutter 也有提供 Material 與 Cupertino 各自 component 的樣式任君挑選,趕快來試試找到心儀的 widget 並實作看看吧!
明天我們會來講解 Flutter widget 的重要概念,Stateless 與 Stateful widget,準備好了嗎!!明天見囉~
Row(
// 水平讓各自 Container 間有間距
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// 垂直靠下
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(color: Colors.red, width: 100, height: 50),
Container(color: Colors.indigo, width: 100, height: 60),
Container(color: Colors.green, width: 100, height: 70),
]))
Column(
// 先考慮 column 佈局
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 第一個 Container 水平靠左
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(color: Colors.red, width: 50, height: 40),
],
),
// 第二個 Container 水平置中
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(color: Colors.indigo, width: 60, height: 40),
],
),
// 第三個 Container 水平靠右
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(color: Colors.green, width: 70, height: 40),
],
),
])